﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LN = Lucene.Net;
using System.Configuration;
using System.Diagnostics;

namespace CYTS.Aoyou.LuceneSearch
{
    public class IndexManager
    {
        private static string indexPath = ConfigurationManager.AppSettings["IndexPath"];
        private const string dateFormat = "yyyy-MM-dd";
        private const int takeCount = 5000;
        private static object lockObj = new object();
        private static LN.Store.FSDirectory dirFS = LN.Store.FSDirectory.Open(new System.IO.DirectoryInfo(indexPath));
        private static int Counter = 0;

        public static event Action BeginCreate;
        public static event Action<int> OneDocCreated;
        public static event Action<int, long> CreateComplete;
        public static event Action BeginOptimize;
        public static event Action<long> OptimizeComplete;
        public static event Action BeginDelete;
        public static event Action<int, long> DeleteComplete;

        /// <summary>
        /// 创建索引
        /// </summary>
        public static void CreateIndexAndOptimize()
        {
            Stopwatch sw = new Stopwatch();
            // 创建
            if (BeginCreate != null)
            {
                BeginCreate();
            }
            sw.Start();
            CreateIndex();
            sw.Stop();
            if (CreateComplete != null)
            {
                CreateComplete(Counter, sw.ElapsedMilliseconds);
            }
            // 优化
            if (BeginOptimize != null)
            {
                BeginOptimize();
            }
            sw.Restart();
            OptimizeIndex();
            sw.Stop();
            if (OptimizeComplete != null)
            {
                OptimizeComplete(sw.ElapsedMilliseconds);
            }
        }

        /// <summary>
        /// 更新索引
        /// </summary>
        public static void UpdateIndex()
        {
            Stopwatch sw = new Stopwatch();
            // 删除并优化
            if (BeginDelete != null)
            {
                BeginDelete();
            }
            sw.Start();
            DeleteIndex();
            OptimizeIndex();
            sw.Stop();
            if (DeleteComplete != null)
            {
                DeleteComplete(Counter, sw.ElapsedMilliseconds);
            }
            // 创建
            CreateIndexAndOptimize();
        }


        public static Result Search(string keyWord, int pageNumber, int count)
        {
            LN.Analysis.Analyzer analyzer = new LN.Analysis.PanGu.PanGuAnalyzer();
            LN.Search.IndexSearcher searcher = new LN.Search.IndexSearcher(dirFS, true);
            string[] fields = new[] 
            {
                "Title","Keywords","Content", "Abstract","Region","Source", "Type", "Id", "Date"
            };
            LN.QueryParsers.MultiFieldQueryParser parser = new LN.QueryParsers.MultiFieldQueryParser(LN.Util.Version.LUCENE_29, fields, analyzer);
            LN.Search.Query query = parser.Parse(keyWord);
            LN.Search.TopScoreDocCollector collector = LN.Search.TopScoreDocCollector.create(pageNumber * count, true);
            searcher.Search(query, collector);

            var hits = collector.TopDocs().scoreDocs;
            var results = hits.Skip((pageNumber - 1) * count).Take(count);

            Result result = new Result();
            result.TotalResult = collector.GetTotalHits();
            result.DocList = new List<Doc>();
            foreach (var hit in results)
            {
                LN.Documents.Document document = searcher.Doc(hit.doc);
                LN.Documents.Field titleField = document.GetField("Title");
                LN.Documents.Field keywordsField = document.GetField("Keywords");
                LN.Documents.Field contentField = document.GetField("Content");
                LN.Documents.Field abstractField = document.GetField("Abstract");
                LN.Documents.Field regionField = document.GetField("Region");
                LN.Documents.Field sourceField = document.GetField("Source");
                LN.Documents.Field typeField = document.GetField("Type");
                LN.Documents.Field idField = document.GetField("Id");
                LN.Documents.Field dateField = document.GetField("Date");

                // 高亮显示设置
                PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter = new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>");
                PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(simpleHTMLFormatter, new PanGu.Segment());
                //关键内容显示大小设置 
                highlighter.FragmentSize = 200;
                //取出高亮显示内容
                string highlightContentSection = highlighter.GetBestFragment(keyWord, contentField.StringValue());
                string highlightTitleSection = highlighter.GetBestFragment(keyWord, titleField.StringValue());
                if (string.IsNullOrEmpty(highlightTitleSection))
                {
                    highlightTitleSection = titleField.StringValue();
                }
                if (string.IsNullOrEmpty(highlightContentSection))
                {
                    int length = contentField.StringValue().Length > highlighter.FragmentSize ? highlighter.FragmentSize : contentField.StringValue().Length;
                    highlightContentSection = contentField.StringValue().Substring(0, highlighter.FragmentSize);
                }

                Doc doc = new Doc();
                doc.Abstract = abstractField.StringValue();
                doc.Content = highlightContentSection;
                doc.Date = DateTime.ParseExact(dateField.StringValue(), dateFormat, null);
                doc.ReplyCount = 0;
                doc.Keywords = keywordsField.StringValue();
                doc.Region = regionField.StringValue();
                doc.Source = sourceField.StringValue();
                doc.Title = highlightTitleSection;
                doc.ViewCount = 0;
                doc.Type = typeField.StringValue();
                doc.Id = string.IsNullOrEmpty(idField.StringValue()) ? 0 : int.Parse(idField.StringValue());
                doc.Uid = doc.Type + doc.Id;
                result.DocList.Add(doc);
            }
            searcher.Close();
            return result;
        }


        private static void OptimizeIndex()
        {
            OptimizeIndex(dirFS);
        }

        /// <summary>
        /// 删除文档
        /// </summary>
        private static void DeleteIndex()
        {
            List<Doc> listDoc = DocManager.GetObsoleteDocs();
            foreach (var doc in listDoc)
            {
                DeleteDocuments(dirFS, "Uid", doc.Uid, false);
            }
        }

        /// <summary>
        /// 多任务创建索引
        /// </summary>
        private static void CreateIndex()
        {
            int docCount = takeCount;
            while (docCount == takeCount)
            {
                List<Doc> listDoc = DocManager.GetFreshDocs(takeCount);
                docCount = listDoc.Count;
                int taskCount = GetTaskCount(docCount);
                List<List<Doc>> groups = GetDocGroups(listDoc, taskCount);
                System.Threading.Tasks.Parallel.For(0, taskCount, n =>
                {
                    CreateIndexWithBuffer(groups[n]);
                });
            }
        }

        /// <summary>
        /// 获取文档分组
        /// </summary>
        /// <param name="listDoc"></param>
        /// <param name="groupCount"></param>
        /// <returns></returns>
        private static List<List<Doc>> GetDocGroups(List<Doc> listDoc, int groupCount)
        {
            List<List<Doc>> groups = new List<List<Doc>>();
            int divide = (int)Math.Ceiling((decimal)listDoc.Count / groupCount);
            for (int i = 0; i < groupCount; i++)
            {
                groups.Add(listDoc.Skip(i * divide).Take(divide).ToList());
            }
            return groups;
        }

        /// <summary>
        /// 计算并行的任务数
        /// </summary>
        /// <param name="docCount"></param>
        /// <returns></returns>
        private static int GetTaskCount(int docCount)
        {
            int taskCount = 0;
            if (docCount <= 8000)
            {
                taskCount = (int)Math.Ceiling(docCount / 2000m);
            }
            else
            {
                taskCount = 5;
            }

            return taskCount;
        }

        /// <summary>
        /// 用缓存方式创建索引
        /// </summary>
        /// <param name="listToIndex"></param>
        private static void CreateIndexWithBuffer(List<Doc> listToIndex)
        {
            var dirRam = new LN.Store.RAMDirectory();
            LN.Analysis.Analyzer analyzer = new LN.Analysis.PanGu.PanGuAnalyzer();
            LN.Index.IndexWriter iw = new LN.Index.IndexWriter(dirRam, analyzer, true, LN.Index.IndexWriter.MaxFieldLength.LIMITED);

            for (int i = 1; i <= listToIndex.Count; i++)
            {
                DocManager.UpdateDocsStatus(listToIndex[i - 1].Type, listToIndex[i - 1].Id);
                var document = GetDocument(listToIndex[i - 1]);
                iw.AddDocument(document);
                if (OneDocCreated != null)
                {
                    OneDocCreated(Counter++);
                }

                if (i % 1000 == 0 || i == listToIndex.Count)
                {
                    iw.Close();
                    MergeIndex(dirRam, dirFS, false);
                    if (i < listToIndex.Count)
                    {
                        dirRam = new LN.Store.RAMDirectory();
                        iw = new LN.Index.IndexWriter(dirRam, analyzer, true, LN.Index.IndexWriter.MaxFieldLength.LIMITED);
                        GC.Collect();
                    }
                }
            }

        }

        /// <summary>
        /// 获取文档
        /// </summary>
        /// <param name="doc"></param>
        /// <returns></returns>
        private static LN.Documents.Document GetDocument(Doc doc)
        {
            LN.Documents.Document document = new LN.Documents.Document();

            var fieldTitle = new LN.Documents.Field("Title", doc.Title, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.ANALYZED);
            fieldTitle.SetBoost(1F);
            document.Add(fieldTitle);

            var fieldKeywords = new LN.Documents.Field("Keywords", doc.Keywords, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.ANALYZED);
            fieldKeywords.SetBoost(0.9F);
            document.Add(fieldKeywords);

            var fieldContent = new LN.Documents.Field("Content", StripTagsCharArray(doc.Content), LN.Documents.Field.Store.YES, LN.Documents.Field.Index.ANALYZED);
            fieldContent.SetBoost(0.8F + (doc.ViewCount + doc.ReplyCount) / 10000F);
            document.Add(fieldContent);

            var fieldAbstract = new LN.Documents.Field("Abstract", doc.Abstract, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.ANALYZED);
            fieldAbstract.SetBoost(0.7F);
            document.Add(fieldAbstract);

            var fieldRegion = new LN.Documents.Field("Region", doc.Region, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NOT_ANALYZED);
            fieldRegion.SetBoost(0.6F);
            document.Add(fieldRegion);

            var fieldSource = new LN.Documents.Field("Source", doc.Source, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NOT_ANALYZED);
            fieldSource.SetBoost(0.1F);
            document.Add(fieldSource);

            var fieldCreateYear = new LN.Documents.Field("CreateYear", LN.Documents.DateTools.DateToString(doc.Date, LN.Documents.DateTools.Resolution.YEAR), LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NOT_ANALYZED);
            fieldCreateYear.SetBoost(0.01F);
            document.Add(fieldCreateYear);

            var fieldCreateMonth = new LN.Documents.Field("CreateMonth", LN.Documents.DateTools.DateToString(doc.Date, LN.Documents.DateTools.Resolution.MONTH), LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NOT_ANALYZED);
            fieldCreateMonth.SetBoost(0.009F);
            document.Add(fieldCreateMonth);

            var fieldCreateDay = new LN.Documents.Field("CreateDay", LN.Documents.DateTools.DateToString(doc.Date, LN.Documents.DateTools.Resolution.DAY), LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NOT_ANALYZED);
            fieldCreateDay.SetBoost(0.008F);
            document.Add(fieldCreateDay);

            var fieldUid = new LN.Documents.Field("Uid", doc.Uid, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NOT_ANALYZED);
            fieldUid.SetBoost(0.01F);
            document.Add(fieldUid);

            var fieldType = new LN.Documents.Field("Type", doc.Type, LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NO);
            document.Add(fieldType);

            var fieldId = new LN.Documents.Field("Id", doc.Id.ToString(), LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NO);
            document.Add(fieldId);

            var fieldDate = new LN.Documents.Field("Date", doc.Date.ToString(dateFormat), LN.Documents.Field.Store.YES, LN.Documents.Field.Index.NO);
            document.Add(fieldDate);

            return document;
        }

        /// <summary>
        /// 合并索引
        /// </summary>
        /// <param name="sourceDir"></param>
        /// <param name="targetDir"></param>
        /// <param name="optimize"></param>
        private static void MergeIndex(LN.Store.Directory sourceDir, LN.Store.Directory targetDir, bool optimize)
        {
            lock (lockObj)
            {
                bool isNew = false;
                if (!LN.Index.IndexReader.IndexExists(targetDir))
                {
                    isNew = true;
                }
                LN.Index.IndexWriter iw = new LN.Index.IndexWriter(targetDir, new LN.Analysis.PanGu.PanGuAnalyzer(), isNew, LN.Index.IndexWriter.MaxFieldLength.LIMITED);
                iw.AddIndexesNoOptimize(new LN.Store.Directory[] { sourceDir });
                if (optimize)
                {
                    iw.Optimize();
                }
                iw.Close();
            }
        }

        /// <summary>
        /// 索引优化
        /// </summary>
        /// <param name="dir"></param>
        private static void OptimizeIndex(LN.Store.Directory dir)
        {
            LN.Index.IndexWriter iw = new LN.Index.IndexWriter(dir, new LN.Analysis.PanGu.PanGuAnalyzer(), false, LN.Index.IndexWriter.MaxFieldLength.LIMITED);
            iw.Optimize();
            iw.Close();
        }

        /// <summary>
        /// 删除文档
        /// </summary>
        /// <param name="dir"></param>
        /// <param name="key"></param>
        /// <param name="optimize"></param>
        private static void DeleteDocuments(LN.Store.Directory dir, string keyField, string value, bool optimize)
        {
            LN.Index.IndexWriter iw = new LN.Index.IndexWriter(dir, new LN.Analysis.PanGu.PanGuAnalyzer(), false, LN.Index.IndexWriter.MaxFieldLength.LIMITED);
            iw.DeleteDocuments(new LN.Index.Term(keyField, value));
            if (optimize)
            {
                iw.Optimize();
            }
            iw.Close();
        }

        /// <summary>
        /// Remove HTML tags from string using char array.
        /// </summary>
        private static string StripTagsCharArray(string source)
        {
            char[] array = new char[source.Length];
            int arrayIndex = 0;
            bool inside = false;

            for (int i = 0; i < source.Length; i++)
            {
                char let = source[i];
                if (let == '<')
                {
                    inside = true;
                    continue;
                }
                if (let == '>')
                {
                    inside = false;
                    continue;
                }
                if (!inside)
                {
                    array[arrayIndex] = let;
                    arrayIndex++;
                }
            }
            return new string(array, 0, arrayIndex);
        }
    }
}
